/*
 * Copyright (C) 1997-2008 ZSNES Team ( zsKnight, _Demo_, pagefault, Nach )
 *
 * http://www.zsnes.com
 * http://sourceforge.net/projects/zsnes
 * https://zsnes.bountysource.com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <string.h>

#include "../cfg.h"
#include "../cpu/memtable.h"
#include "../init.h"
#include "../ui.h"
#include "c_gui.h"
#include "c_guiwindp.h"
#include "gui.h"
#include "guicheat.h"
#include "guikeys.h"
#include "guiwindp.h"


u1 CopyRamToggle;

static u1 FirstSearch;


static void set_cheat_data(u1* const c, u1 const toggle, u1 const value, u1 const address, u1 const pvalue)
{
	c[0]          = toggle;
	c[1]          = value;
	*(u2*)(c + 2) = address;
	c[4]          = address >> 16;
	c[5]          = pvalue;
}


static void AddCheatCode(u4 const eax, u1 const bl)
{
	GUICBHold = 0;
	if (NumCheats == 255) return;

	u1* const edx = cheatdata + NumCheats++ * 28;
	{ // transfer description
		u1*         eax = edx;
		char const* ebx = CSDescDisplay;
		u4          ecx = 20;
		do
		{
			char const dl = *ebx++;
			eax[8]          = dl;
			eax[8 + 18]     = dl;
			eax[8 + 18 * 2] = dl;
		}
		while (++eax, --ecx != 0);
	}
	// toggle, value, address, pvalue, name(12)
	u1* const eax_ = wramdata + (eax - 0x7E0000);
	u1  const bh   = *eax_;
	*eax_ = bl;
	set_cheat_data(edx, 0, bl, eax, bh);

	u1 const al = GUIpmenupos;
	CheckMenuItemHelp(7);
	GUIpmenupos = al;
	CheatOn     = 1;
}


void AddCSCheatCode(void)
{
	if (CSInputDisplay[0] == '_') return;

	curaddrvalcs = curentryval;
	curvaluecs   = CSCurValue;
	u4 ecx = CheatSrcByteSize + 1;
	if (CheatUpperByteOnly != 0)
	{
		ecx = 1;
		while (curvaluecs >= 0xFF)
		{
			curvaluecs >>= 8;
			++curaddrvalcs;
		}
	}
	do
	{
		u4       eax = curaddrvalcs + 0x7E0000;
		u1 const bl  = curvaluecs;
		// write bl at address eax
		AddCheatCode(eax, bl);

		curvaluecs >>= 8;
		++curaddrvalcs;
	}
	while (--ecx != 0);
	CheatWinMode = 2;
}


#define SearchMacro(CMP)    \
do                          \
{                           \
	u1 dl = 0xFE;             \
	do                        \
	{                         \
		if (!(CMP)) *eax &= dl; \
		++edi;                  \
		++esi;                  \
		if (dl == 0x7F) ++eax;  \
		dl = dl << 1 | dl >> 7; \
	}                         \
	while (--ecx != 0);       \
}                           \
while (0)


#define SearchMacroB(L, R)                                   \
{                                                            \
	switch (CheatCompareValue)                                 \
	{                                                          \
		case 0:  SearchMacro((L) >  (R)); break; /* Greater   */ \
		case 1:  SearchMacro((L) <  (R)); break; /* Less      */ \
		case 3:  SearchMacro((L) != (R)); break; /* Not equal */ \
		default: SearchMacro((L) == (R)); break; /* Equal     */ \
	}                                                          \
}


void CheatCodeSearchProcess(void)
{
	if (CheatSrcSearchType == 1)
	{ // Comparative
		CSInputDisplay[0] = '_';
		CSInputDisplay[1] = '\0';
		u1*       eax = vidbuffer + 129600 + 65536 * 2;
		u1 const* esi = vidbuffer + 129600;
		u1 const* edi = wramdata;
		u4        ecx = 65536 * 2;
		switch (CheatSrcByteSize)
		{
			default: ecx -= 0; SearchMacroB(*(u1 const*)edi,              *(u1 const*)esi);              break;
			case 1:  ecx -= 1; SearchMacroB(*(u2 const*)edi,              *(u2 const*)esi);              break;
			case 2:  ecx -= 2; SearchMacroB(*(u4 const*)edi & 0x00FFFFFF, *(u2 const*)esi & 0x00FFFFFF); break;
			case 3:  ecx -= 3; SearchMacroB(*(u4 const*)edi,              *(u4 const*)esi);              break;
		}
		CopyRamToggle = 1;
	}
	else if (CSInputDisplay[0] != '\0' && CSInputDisplay[0] != '_' && CSOverValue != 1)
	{
		CSInputDisplay[0] = '_';
		CSInputDisplay[1] = '\0';
		// Process Cheat Search
		u1 const  ebx = CheatSrcByteSize;
		u4        ecx = 65536 * 2 - ebx;
		u4 const  edx = SrcMask[ebx];
		u1*       edi = vidbuffer + 129600 + 65536 * 2;
		u1 const* esi = wramdata;
		u1        bl  = 0xFE;
		u1        bh  = 0x01;
		do
		{
			u4 const eax = *(u4 const*)esi & edx; // XXX unaligned
			if (eax == CSCurValue)
			{
				if (!(edi[16384] & bh)) goto failedfind;
			}
			else if ((eax + 1) & edx == CSCurValue)
			{
				if (FirstSearch == 1)
				{
					edi[16384] &= bl;
				}
				else
				{
					if (edi[16384] & bh) goto failedfind;
				}
			}
			else
			{
failedfind:
				*edi &= bl;
			}
			if (bl == 0x7F) ++edi;
			bl = bl << 1 | bl >> 7;
			bh = bh << 1 | bh >> 7;
		}
		while (++esi, --ecx != 0);
		CopyRamToggle     = 1;
		CheatSearchStatus = 1;
	}
}


void CheatCodeSearchInit(void)
{
	CSInputDisplay[0] = '_';
	CSInputDisplay[1] = '\0';
	CheatWinMode      = 1;
	CheatSearchStatus = 0;
	FirstSearch       = 1;
	// copy 128k ram
	memcpy(vidbuffer + 129600, wramdata, 131072);
	// fill searched buffer with 0xFF
	memset(vidbuffer + 129600 + 65536 * 2, 0xFF, 32768 * 4);
	if (CheatSrcSearchType == 1) CheatSearchStatus = 1;
	CheatCompareValue = 0;
	u1 val;
	switch (CheatSrcByteSize)
	{
		case 1: val = 0x7F; break;
		case 2: val = 0x3F; break;
		case 3: val = 0x1F; break;
		default: return;
	}
	vidbuffer[129600 + 65536 * 2 + 16383] &= val;
}


static void DisableCheatCode(u1* const esi)
{
	esi[0] |= 0x04;
	if (esi[0] & 0x01)
	{
		u4 const ecx = *(u4 const*)(esi + 2) & 0x00FFFFFF; // XXX unaligned
		romdata[ecx] = esi[5];
	}
	else if (!(esi[0] & 0x80) && !(esi[-28] & 0x80))
	{
		memw8(esi[4], *(u2 const*)(esi + 2), esi[5]);
	}
}


void DisableCheatsOnLoad(void)
{
	// Disable all codes
	u1* esi = cheatdata;
	for (u4 ecx = NumCheats; ecx != 0; esi += 28, --ecx)
	{
		if (esi[0] & 0x04) continue;
		DisableCheatCode(esi);
	}
}


static void EnableCheatCode(u1* const esi)
{
	esi[0] &= 0xFB;
	if (esi[0] & 0x01)
	{
		u1  const al  = esi[1];
		u4  const ecx = *(u4 const*)(esi + 2) & 0x00FFFFFF; // XXX unaligned
		u1* const esi = romdata;
		u1  const bl  = esi[ecx];
		esi[ecx] = al;
		esi[5]   = bl;
	}
	else
	{
		u1 const al = esi[1];
		u2 const cx = *(u2 const*)(esi + 2);
		u1 const bl = esi[4];
		esi[5] = memr8(bl, cx);
		if (!(esi[0] & 0x80) && !(esi[-28] & 0x80)) memw8(bl, cx, al);
	}
}


void EnableCheatsOnLoad(void)
{
	// Enable all ON toggled cheat codes
	u1* esi = cheatdata;
	for (u4 ecx = NumCheats; ecx != 0; esi += 28, --ecx)
	{
		if (esi[0] & 0x04) continue;
		EnableCheatCode(esi);
	}
}


void CheatCodeRemove(void)
{
	GUICBHold = 0;
	if (NumCheats == 0) return;

	u1* const esi  = cheatdata + GUIcurrentcheatcursloc * 28;
	DisableCheatCode(esi);
	memmove(esi, esi + 28, (255 - GUIcurrentcheatcursloc) * 18); // XXX 18? Probably should be 28

	u4 const eax = GUIcurrentcheatcursloc;
	if (--NumCheats != 0 && eax == NumCheats)
	{
		GUIcurrentcheatcursloc = eax -  1;
		GUIcurrentcheatviewloc = eax - 12;
		if (GUIcurrentcheatviewloc & 0x80000000) GUIcurrentcheatviewloc = 0;
	}

	if (NumCheats == 0) CheatOn = 0;
}


void CheatCodeFix(void)
{
	GUICBHold = 0;
	if (NumCheats == 0) return;

	u1* const esi = cheatdata + GUIcurrentcheatcursloc * 28;
	DisableCheatCode(esi);
	esi[3] ^= 0x80;
	EnableCheatCodeNoPrevMod(esi);
}


void CheatCodeToggle(void)
{
	GUICBHold = 0;
	if (NumCheats == 0) return;

	u1* const esi = cheatdata + GUIcurrentcheatcursloc * 28;
	if (esi[0] & 0x04)
	{
		EnableCheatCodeNoPrevMod(esi);
	}
	else
	{
		DisableCheatCode(esi);
	}
}


void EnableCheatCodeNoPrevMod(u1* const esi)
{
	// code is at esi
	esi[0] &= 0xFB;
	if (esi[0] & 0x01)
	{
		u1  const al  = esi[1];
		u4  const ecx = *(u4 const*)(esi + 2) & 0x00FFFFFF; // XXX unaligned
		u1* const esi = romdata;
		u1  const bl  = esi[ecx];
		esi[ecx] = al;
		esi[5]   = bl;
	}
	else if (!(esi[0] & 0x80) && !(esi[-28] & 0x80))
	{
		memw8(esi[4], *(u2 const*)(esi + 2), esi[1]);
	}
}


static void decodepar(u1 const guicheatvalrep)
{
	// convert code to number format
	u4    ecx = 8;
	char* esi = GUICheatTextZ1;
	do
	{
		char const al = *esi;
		*esi++ = al < 'A' ? al - '0' : al - 'A' + 10;
	}
	while (--ecx != 0);

	// get address
	u1 const bl =
		(u1)GUICheatTextZ1[0] <<  4 |
		(u1)GUICheatTextZ1[1] <<  0;
	u2 const cx =
		(u1)GUICheatTextZ1[2] << 12 |
		(u1)GUICheatTextZ1[3] <<  8 |
		(u1)GUICheatTextZ1[4] <<  4 |
		(u1)GUICheatTextZ1[5] <<  0;
	u1 const al =
		(u1)GUICheatTextZ1[6] <<  4 |
		(u1)GUICheatTextZ1[7] <<  0;

	// store into cheatdata
	u1* const edx = cheatdata + NumCheats * 28;
	set_cheat_data(edx, guicheatvalrep, al, bl << 16 | cx, memr8(bl, cx));

	if (!(edx[0] & 0x80) && !(edx[-28] & 0x80)) memw8(bl, cx, al);

	CheatOn = 1;
	++NumCheats;
	memset(GUICheatTextZ1, '\0', 4);
	memset(GUICheatTextZ2, '\0', 4);
	GUICheatPosA       = 0;
	GUICheatPosB       = 0;
	GUIcurrentcheatwin = 1;
	u4 const eax = NumCheats;
	GUIcurrentcheatcursloc = eax -  1;
	GUIcurrentcheatviewloc = eax - 12;
	if (GUIcurrentcheatviewloc & 0x80000000) GUIcurrentcheatviewloc = 0;
}


static void decodegg(u1 const guicheatvalrep)
{
	/* Genie Hex:    D  F  4  7  0  9  1  5  6  B  C  8  A  2  3  E
	 * Normal  Hex:  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
	 *               4  6  D  E  2  7  8  3  B  5  C  9  A  0  F  1 */
	{ // Convert code
		char* esi = GUICheatTextZ1;
		memmove(esi + 4, esi + 5, 4);
		// convert code to number format
		u4 ecx = 8;
		do
		{
			static u1 const GG2Norm[] = { 0x4, 0x6, 0xD, 0xE, 0x2, 0x7, 0x8, 0x3, 0xB, 0x5, 0xC, 0x9, 0xA, 0x0, 0xF, 0x1 };
			char const al = *esi;
			*esi++ = GG2Norm[al < 'A' ? al - '0' : al - 'A' + 10];
		}
		while (--ecx != 0);
	}

	char const* const esi = GUICheatTextZ1;
	u4          const ecx =
		(u1)esi[2] << 20 |
		(u1)esi[3] << 16 |
		(u1)esi[4] << 12 |
		(u1)esi[5] <<  8 |
		(u1)esi[6] <<  4 |
		(u1)esi[7] <<  0;

	/*                        0123456789ABCDEF01234567
	 * 24bit encoded address: ijklqrstopabcduvwxefghmn
	 *                        abcdefghijklmnopqrstuvwx
	 *                        >8  >12 >6<10 >6  <14 <10 */
	u4 const ebx =
		(ecx & 0x003C00) << 10 | // abcd
		(ecx & 0x00003C) << 14 | // efgh
		(ecx & 0xF00000) >>  8 | // ijkl
		(ecx & 0x000003) << 10 | // mn
		(ecx & 0x00C000) >>  6 | // op
		(ecx & 0x0F0000) >> 12 | // qrst
		(ecx & 0x0003C0) >>  6;  // uvwx
	u1 const al = (u1)esi[0] << 1 | (u1)esi[1];

	// store into cheatdata
	u1* const edx = cheatdata + NumCheats * 28;
	set_cheat_data(edx, guicheatvalrep, al, ebx, memr8(ebx >> 16, ebx));

	if (!(edx[0] & 0x80) && !(edx[-28] & 0x80)) memw8(ebx >> 16, ebx, al);

	CheatOn = 1;
	++NumCheats;
	memset(GUICheatTextZ1, '\0', 4);
	memset(GUICheatTextZ2, '\0', 4);
	GUICheatPosA       = 0;
	GUICheatPosB       = 0;
	GUIcurrentcheatwin = 1;
	u4 const eax = NumCheats;
	GUIcurrentcheatcursloc = eax -  1;
	GUIcurrentcheatviewloc = eax - 12;
	if (GUIcurrentcheatviewloc & 0x80000000) GUIcurrentcheatviewloc = 0;
}


static void decodegf(u1 const guicheatvalrep)
{
	{ // convert code to number format
		u4    ecx = 14;
		char* esi = GUICheatTextZ1;
		do
		{
			char const al = *esi;
			if (al != 'X') *esi = al < 'A' ? al - '0' : al - 'A' + 10;
		}
		while (++esi, --ecx != 0);
	}

	// get address
	u4 ecx =
		(u1)GUICheatTextZ1[0] << 16 |
		(u1)GUICheatTextZ1[1] << 12 |
		(u1)GUICheatTextZ1[2] <<  8 |
		(u1)GUICheatTextZ1[3] <<  4 |
		(u1)GUICheatTextZ1[4] <<  0;

	u1* edx = cheatdata + NumCheats * 28;

	// Write data to memory
	if (GUICheatTextZ1[13] == 1)
	{
		if (ecx > 65535) goto quit;
		u1* const esi = sram;

		// get data
		if (GUICheatTextZ1[5] != 'X' && GUICheatTextZ1[6] != 'X')
		{
			u1 const bl = GUICheatTextZ1[5] << 4 | GUICheatTextZ1[6];
			u1 const al = esi[ecx];
			esi[ecx] = bl;
			set_cheat_data(edx, 2, bl, ecx, al);
			++NumCheats;
			edx += 28;
		}

		++ecx;
		if (GUICheatTextZ1[7] != 'X' && GUICheatTextZ1[8] != 'X' && NumCheats != 255 && ecx <= 65535)
		{
			u1 const bl = GUICheatTextZ1[7] << 4 | GUICheatTextZ1[8];
			u1 const al = esi[ecx + 1];
			esi[ecx + 1] = bl;
			set_cheat_data(edx, 2, bl, ecx, al);
			++NumCheats;
			edx += 28;
		}

		++ecx;
		if (GUICheatTextZ1[9] != 'X' && GUICheatTextZ1[10] != 'X' && NumCheats != 255 && ecx <= 65535)
		{
			u1 const bl = GUICheatTextZ1[9] << 4 | GUICheatTextZ1[10];
			u1 const al = esi[ecx + 2];
			esi[ecx + 2] = bl;
			set_cheat_data(edx, 2, bl, ecx, al);
			++NumCheats;
		}
	}
	else
	{
		// get data
		if (GUICheatTextZ1[5] != 'X' && GUICheatTextZ1[6] != 'X')
		{
			u1  const bl  = GUICheatTextZ1[5] << 4 | GUICheatTextZ1[6];
			u1* const esi = romdata;
			u1  const al  = esi[ecx];
			esi[ecx] = bl;
			set_cheat_data(edx, guicheatvalrep | 1, bl, ecx, al);
			++NumCheats;
			edx += 28;
		}

		++ecx;
		if (GUICheatTextZ1[7] != 'X' && GUICheatTextZ1[8] != 'X' && NumCheats != 255)
		{
			u1  const bl  = GUICheatTextZ1[7] << 4 | GUICheatTextZ1[8];
			u1* const esi = romdata;
			u1  const al  = esi[ecx];
			esi[ecx] = bl;
			set_cheat_data(edx, 1, bl, ecx, al);
			++NumCheats;
			edx += 28;
		}

		++ecx;
		if (GUICheatTextZ1[9] != 'X' && GUICheatTextZ1[10] != 'X' && NumCheats != 255)
		{
			u1  const bl  = GUICheatTextZ1[9] << 4 | GUICheatTextZ1[10];
			u1* const esi = romdata;
			u1  const al  = esi[ecx];
			esi[ecx] = bl;
			set_cheat_data(edx, 1, bl, ecx, al);
			++NumCheats;
		}
	}

quit:
	if (NumCheats != 0)
	{
		CheatOn            = 1;
		GUIcurrentcheatwin = 1;
		u4 const eax = NumCheats;
		GUIcurrentcheatcursloc = eax -  1;
		GUIcurrentcheatviewloc = eax - 12;
		if (GUIcurrentcheatviewloc & 0x80000000) GUIcurrentcheatviewloc = 0;
	}
	memset(GUICheatTextZ1, 0, 4);
	memset(GUICheatTextZ2, 0, 4);
	GUICheatPosA = 0;
	GUICheatPosB = 0;
}


void ProcessCheatCode(void)
{
	GUICBHold = 0;
	if (NumCheats    == 255) return;
	if (GUICheatPosA ==   0) return;

	// make sure flashing cursor doesn't exist
	GUICheatTextZ2[GUICheatPosB] = '\0';
	{ // transfer description
		u1*         eax = cheatdata + NumCheats * 28 + 6;
		char const* ebx = GUICheatTextZ2;
		u4          ecx = 20;
		do
		{
			char const dl = *ebx++;
			eax[         2] = dl;
			eax[18     + 2] = dl;
			eax[18 * 2 + 2] = dl;
		}
		while (++eax, --ecx != 0);
	}

	{ // uppercase all codes if necessary
		char* eax = GUICheatTextZ1;
		u4    ecx = 14;
		do
		{
			char const bl = *eax;
			if ('a' <= bl && bl <= 'z') *eax = bl - 'a' + 'A';
		}
		while (++eax, --ecx != 0);
	}

	u1 guicheatvalrep = 0;
	if (GUICheatTextZ1[GUICheatPosA - 1] == 'R')
	{
		guicheatvalrep = 0x80;
		--GUICheatPosA;
	}

	// determine whether it is gamegenie, par, or GF
	switch (GUICheatPosA)
	{
		case 8: // PAR
			{ // check if code is valid
				char const* eax = GUICheatTextZ1;
				u4          ecx = 8;
				do
				{
					char const bl = *eax++;
					if ('0' <= bl && bl <= '9') continue;
					if ('A' <= bl && bl <= 'F') continue;
					goto invalid;
				}
				while (--ecx != 0);
			}
			decodepar(guicheatvalrep);
			break;

		case 9: // GG
			{ // check if code is valid
				char const* eax = GUICheatTextZ1;
				u4          ecx = 9;
				do
				{
					if (ecx == 5)
					{
						if (*eax != '-') goto invalid;
						++eax;
						--ecx;
					}
					char const bl = *eax++;
					if ('0' <= bl && bl <= '9') continue;
					if ('A' <= bl && bl <= 'F') continue;
					goto invalid;
				}
				while (--ecx != 0);
			}
			decodegg(guicheatvalrep);
			break;

		case 14: // GF
			{ // check if code is valid
				char const* eax = GUICheatTextZ1;
				u4          ecx = 5;
				do
				{
					char const bl = *eax++;
					if ('0' <= bl && bl <= '9') continue;
					if ('A' <= bl && bl <= 'F') continue;
					goto invalid;
				}
				while (--ecx != 0);
			}
			{ char const* eax = GUICheatTextZ1 + 5;
				u4          ecx = 6;
				do
				{
					char const bl = *eax++;
					if (bl == 'X') continue;
					if ('0' <= bl && bl <= '9') continue;
					if ('A' <= bl && bl <= 'F') continue;
					goto invalid;
				}
				while (--ecx != 0);
				char const bl = GUICheatTextZ1[13];
				if (bl != '0' && bl != '1') goto invalid;
			}
			decodegf(guicheatvalrep);
			break;

		default:
invalid:
			guicheaterror();
			break;
	}
}
